package com.mtconnect.util;

import com.mtconnect.model.Condition;
import com.mtconnect.model.Event;
import com.mtconnect.model.Sample;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.SAXReader;
import org.dom4j.io.SAXValidator;
import org.dom4j.io.XMLWriter;
import org.dom4j.tree.DefaultText;
import org.dom4j.util.XMLErrorHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import javax.xml.parsers.*;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import java.io.*;
import java.net.URL;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Created by 赵鲁 on 2017/6/28.
 * <p>
 * xml file validator and load
 * <p>
 * 关于使用dom4j向带有命名空间的XML文档中进行添加元素。
 * 如果是先创建再添加的元素会带有一个默认的命名空间。例如：
 * <p>
 * <p>
 * Element oneBook = document.getRootElement().element("书");
 * Element price = DocumentHelper.createElement("售价");
 * price.setText("209元");
 * oneBook.addElement(price);
 * <p>
 * 如上添加的元素会带有一个默认的命名空间，如下：
 * <售价 xmlns="">209元</售价>
 * <p>
 * 可以先在文档中添加这个元素，然后对这个元素进行操作，再次添加到文档中方法，就不会有上面的默认命名空间
 * <p>
 * 如下：
 * Element oneBook = document.getRootElement().element("书");
 * oneBook.addElement("售价").setText("209元");
 * <p>
 * 当然，有时候我们希望指定这个元素插入的位置，我们可以先获取这个元素的引用，然后从文档中删除这个元素即可，
 * 我们就可以操作这个引用，而删除的只是在文档节点中删除掉了。并不是从内存中将其删除，所以我们还是可以操作这个引用的。
 * <p>
 * 如下：
 * Element oneBook = document.getRootElement().element("书");
 * Element price = oneBook.addElement("售价");
 * price.getParent().remove(price);
 * price.setText("209元");
 * List list = oneBook.elements();
 * list.add(0, price);
 * 如此我们就将这个元素插入到了首位。
 */
public class XMLUtil {

    private static final Logger LOGGER = LoggerFactory.getLogger(XMLUtil.class);

    public static final String XSD = "xsd";
    public static final String DTD = "dtd";

    /**
     * 将元素转换成文档
     *
     * @param element 需要转换的元素
     * @return 转换完成的文档
     * @throws DocumentException 转换发生错误。
     */
    public static Document asDoc(Element element) throws DocumentException {
        return DocumentHelper.parseText(element.asXML());
    }

    /**
     * 读取xml文件
     *
     * @param xmlPath xml文件的绝对路径
     * @return document对象
     * @throws DocumentException 路径为空字符或者null亦或读取错误发生抛出此异常
     */
    public static Document loadXML(String xmlPath) throws DocumentException {
        if (null != xmlPath && !"".equals(xmlPath)) {//判断路径是空或者null
            try {
                return loadXML(new FileInputStream(new File(xmlPath)));
            } catch (FileNotFoundException e) {
                throw new DocumentException("The XML file not exists", e);
            }
        } else {
            throw new DocumentException("The XML file path is null or \"\" ");
        }
    }

    /**
     * 读取xml文件
     *
     * @param xml 包含xml文件的输入流
     * @return document对象
     * @throws DocumentException 输入流是null亦或读取错误发生抛出此异常
     */
    public static Document loadXML(InputStream xml) throws DocumentException {
        if (null != xml) {//判断对象null
            SAXReader saxReader = new SAXReader();
           /* Map<String, String> map = new HashMap<>();
            map.put("mt", "urn:mtconnect.org:MTConnectDevices:1.3");
            saxReader.getDocumentFactory().setXPathNamespaceURIs(map);*/
            return saxReader.read(xml);
        } else {
            throw new DocumentException("The input stream is null");
        }
    }

    public static Document loadXML(File file) throws DocumentException {
        return loadXML(file.getPath());
    }

    /**
     * 将XML文件从内存写入硬盘
     *
     * @param document 需要写入的xml对象
     * @param filePath 要写入的文件路径
     * @return true 写入成功 false 写入失败
     */
    public static boolean writeXML(Document document, String filePath) {
        boolean flag = true;
        try {
            XMLWriter writer = new XMLWriter(new OutputStreamWriter(new FileOutputStream(filePath), "UTF-8"), new OutputFormat().createPrettyPrint());
            writer.write(document);
            writer.flush();
            writer.close();
        } catch (Exception ex) {
            flag = false;
            ex.printStackTrace();
        }
        return flag;
    }

    /**
     * 验证xml文档的完整性，以及是否符合验证规则
     *
     * @param document 需要验证的文档
     * @param rule     xsd 或者 dtd 字符串
     * @return true 完整且符合验证规则 false不完整或者不符合验证规则。
     * @throws DocumentException
     */
    public static boolean validatorXML(Document document, URL rule) throws IOException, SAXException, ParserConfigurationException {
        if (XSD.equals(analysisXMLRuleType(document))) {
            return validatorXMLByXSD(document, rule);
        } else {
            //return validatorXMLByDTD(document, rule);
            return false;
        }
    }

    private static boolean validatorXMLByXSD(Document document, URL rule) throws ParserConfigurationException, SAXException, IOException {
        //http://www.w3.org/XML/XMLSchema/v1.1，这个是1.1版本的
        SchemaFactory sf = SchemaFactory.newInstance("http://www.w3.org/XML/XMLSchema/v1.1");
        Schema schema = sf.newSchema(rule);
        //创建默认的XML错误处理器
        XMLErrorHandler errorHandler = new XMLErrorHandler();
        //获取基于 SAX 的解析器的实例
        SAXParserFactory factory = SAXParserFactory.newInstance();
        //指定由此代码生成的解析器将提供对 XML 名称空间的支持。
        factory.setNamespaceAware(true);
        //设置验证模式
        factory.setSchema(schema);
        //使用当前配置的工厂参数创建 SAXParser 的一个新实例。
        SAXParser parser = factory.newSAXParser();
        //创建一个SAXValidator校验工具，并设置校验工具的属性
        SAXValidator validator = new SAXValidator(parser.getXMLReader());
        //设置校验工具的错误处理器，当发生错误时，可以从处理器对象中得到错误信息。
        validator.setErrorHandler(errorHandler);
        //校验
        validator.validate(document);
        //如果错误信息不为空，说明校验失败，打印错误信息
        if (errorHandler.getErrors().hasContent()) {
            LOGGER.warn("The XML file validator failed!");
            LOGGER.warn(errorHandler.getErrors().asXML());
            return false;
        } else {
            LOGGER.debug("Good! The XML pass validator!");
            return true;
        }
    }

    private static boolean validatorXMLByDTD(Document document, final String rule) throws ParserConfigurationException, IOException, SAXException {
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        factory.setValidating(true);
        DocumentBuilder builder = factory.newDocumentBuilder();
        XMLErrorHandler errorHandler = new XMLErrorHandler();
        builder.setErrorHandler(errorHandler);
        if (null != rule) {
            builder.setEntityResolver((String publicId, String systemId) -> new InputSource(rule));
        }
        builder.parse(new ByteArrayInputStream(document.asXML().getBytes()));
        if (errorHandler.getErrors().hasContent()) {
            System.err.print(errorHandler.getErrors());
            return false;
        } else {
            return true;
        }
    }

    /**
     * 分析xml文件的验证规则
     *
     * @param document 需要被分析的xml文档对象
     * @return xsd 或者 dtd 字符串
     */
    private static String analysisXMLRuleType(Document document) {
        if (null == document.getDocType()) {
            return XSD;
        } else {
            return DTD;
        }
    }

    private static String analysisRuleType(String rule) {
        if (rule.endsWith(XSD)) {
            return XSD;
        } else if (rule.endsWith(DTD)) {
            return DTD;
        } else {
            return null;
        }
    }

    /**
     * 创建一个元素
     *
     * @param name      元素名称
     * @param attribute 元素属性
     * @return 创建完成的元素
     */
    public static Element createElement(String name, Map<String, String> attribute) {
        Element element = DocumentHelper.createElement(name);
        for (Map.Entry<String, String> entry : attribute.entrySet()) {
            element.addAttribute(entry.getKey(), entry.getValue());
        }
        return element;
    }

    /**
     * 创建sample以及current函数返回的元素
     * <p>
     * 会先创建一个父标签Samples，遍历传入的samples集合，然后向Samples标签中添加tag名称的子标签，
     * 每个sample对象对应一个子标签的属性以及CDATA
     * 如果属性值为null，则不予添加
     *
     * @param samples xml标签的属性
     * @param tag     xml标签的名称
     * @return 创建好的Samples元素
     */
    public static Element createSamplesElement(List<Sample> samples, String tag) {
        Element element = DocumentHelper.createElement("Samples");
        for (Sample sample : samples) {
            Element child = element.addElement(tag);
            Map<String, String> attributes = new HashMap<>();
            if (sample.getName() != null) {
                attributes.put("name", sample.getName());
            }
            if (sample.getSequence() != null) {
                attributes.put("sequence", String.valueOf(sample.getSequence()));
            }
            if (sample.getTimestamp() != null) {
                attributes.put("timestamp", DateUtil.getUTCFormat(sample.getTimestamp().toLocalDateTime()));
            }
            if (null != sample.getDataItemID()) {
                attributes.put("dataItemId", sample.getDataItemID());
            }
            if (null != sample.getSubType()) {
                attributes.put("subTypeBySample", sample.getSubType());
            }
            if (null != sample.getSampleRate()) {
                attributes.put("sampleRate", sample.getSampleRate());
            }
            if (null != sample.getStatistic()) {
                attributes.put("statistic", sample.getStatistic());
            }
            if (null != sample.getDuration()) {
                attributes.put("duration", sample.getDuration().toString());
            }
            if (null != sample.getSampleCount()) {
                attributes.put("sampleCount", sample.getSampleCount().toString());
            }
            additionalAttributes(child, attributes);
            if (null != sample.getValue()) {
                child.add(new DefaultText(sample.getValue()));
            }

        }
        return element;
    }

    /**
     * 创建sample以及current函数返回的元素
     * <p>
     * 会先创建一个父标签Events，遍历传入的events集合，然后向Events标签中添加tag名称的子标签，
     * 每个event对象对应一个子标签的属性以及CDATA
     * 如果属性值为null，则不予添加
     *
     * @param events xml标签的属性
     * @param tag    xml标签的名称
     * @return 创建好的Events元素
     */
    public static Element createEventElement(List<Event> events, String tag) {
        Element element = DocumentHelper.createElement("Events");
        for (Event event : events) {
            Element child = element.addElement(tag);
            Map<String, String> attributes = new HashMap<>();
            if (null != event.getName()) {
                attributes.put("name", event.getName());
            }
            if (null != event.getSequence()) {
                attributes.put("sequence", String.valueOf(event.getSequence()));
            }
            if (event.getTimestamp() != null) {
                attributes.put("timestamp", DateUtil.getUTCFormat(event.getTimestamp().toLocalDateTime()));
            }
            if (null != event.getDataItemID()) {
                attributes.put("dataItemId", event.getDataItemID());
            }
            if (null != event.getSubType()) {
                attributes.put("subTypeBySample", event.getSubType());
            }
            if (null != event.getAssetType()) {
                attributes.put("assetType", event.getAssetType());
            }
            additionalAttributes(child, attributes);
            if (null != event.getValue()) {
                child.add(new DefaultText(event.getValue()));
            }
        }
        return element;
    }

    /**
     * 创建sample以及current函数返回的元素
     * <p>
     * 会先创建一个父标签Condition，遍历传入的conditions集合，
     * 然后向Condition标签中添加condition对象中WarningType属性作为名称的子标签，
     * 剩余condition对象中的属性对应一个子标签的属性以及CDATA
     * 如果属性值为null，则不予添加
     *
     * @param conditions xml标签的属性
     * @return 创建好的Condition元素
     */
    public static Element createConditionsElement(List<Condition> conditions) {
        Element element = DocumentHelper.createElement("Condition");
        for (Condition condition : conditions) {
            Element child = element.addElement(StringUtil.underline2Camel(condition.getWarningType(), false));
            Map<String, String> attributes = new HashMap<>();
            if (null != condition.getName()) {
                attributes.put("name", condition.getName());
            }
            if (null != condition.getSequence()) {
                attributes.put("sequence", String.valueOf(condition.getSequence()));
            }
            if (condition.getTimestamp() != null) {
                attributes.put("timestamp", DateUtil.getUTCFormat(condition.getTimestamp().toLocalDateTime()));
            }
            if (null != condition.getDataItemID()) {
                attributes.put("dataItemId", condition.getDataItemID());
            }
            if (null != condition.getSubType()) {
                attributes.put("subTypeBySample", condition.getSubType());
            }
            if (null != condition.getQualifier()) {
                attributes.put("qualifier", condition.getQualifier());
            }
            if (null != condition.getNativeCode()) {
                attributes.put("nativeCode", condition.getNativeCode());
            }
            if (null != condition.getNativeSeverity()) {
                attributes.put("nativeSeverity", condition.getNativeSeverity());
            }
            if (null != condition.getStatistic()) {
                attributes.put("statistic", condition.getStatistic());
            }
            additionalAttributes(child, attributes);
            if (null != condition.getValue()) {
                child.add(new DefaultText(condition.getValue()));
            }
        }
        return element;
    }

    /**
     * 给元素添加属性
     *
     * @param element    需要添加属性的元素
     * @param attributes 属性键值对集合
     * @return 添加完成的元素
     */
    public static Element additionalAttributes(Element element, Map<String, String> attributes) {
        for (Map.Entry<String, String> entry : attributes.entrySet()) {
            element.addAttribute(entry.getKey(), entry.getValue());
        }
        return element;
    }
}

